# -*- coding: utf8 -*-
import csv
import datetime
import jinja2
import json
import logging
import math
import os
import re
import urllib2
import webapp2
from collections import deque
from collections import namedtuple

import models
from BeautifulSoup import BeautifulSoup

from google.appengine.api import images
from google.appengine.api import memcache
from google.appengine.api import taskqueue
from google.appengine.api import users
from google.appengine.datastore.datastore_query import Cursor
from google.appengine.ext import blobstore
from google.appengine.ext.webapp import blobstore_handlers
from google.appengine.ext import ndb
from google.appengine.ext.ndb import metadata

jinja_environment = jinja2.Environment(
    loader=jinja2.FileSystemLoader(os.path.dirname(__file__)))

DATE_FMT = '%Y-%m-%d'

RUNWAY_RE = re.compile('^([0-9][0-9]?)([rRlLcC]?)$')
GEO_RE = re.compile(r'^([NSEW])(\d+).(\d+\.?\d+?).$', re.UNICODE)
CARDINAL_MULTIPLIER = {'N': 1, 'S': -1, 'E': 1, 'W': -1}
XC_REQUIRED_DISTANCE = 92.6  # km


class Currency(object):
  """Calculate various currency requirements for a given pilot."""

  _under40_medical_time = datetime.timedelta(days=365.2425 * 5)
  _over40_medical_time = datetime.timedelta(days=365.2425 * 2)
  _40years = datetime.timedelta(days=365.2425 * 40)
  valid = 'success'
  expiring = 'warning'
  expired = 'danger'

  def __init__(self, pilot, bfr, stats):
    self._pilot = pilot
    self._bfr = bfr
    self._stats = stats
    self._today = datetime.date.today()

  @staticmethod
  def _LastDayOfMonth(date):
    next_month = (date.month + 1) % 12
    return date.replace(month=next_month, day=1) - datetime.timedelta(days=1)

  def _Status(self, due_date, valid_days):
    if due_date - self._today > datetime.timedelta(days=valid_days):
      return (self.valid, due_date)
    elif due_date < self._today:
      return (self.expired, due_date)
    else:
      return (self.expiring, due_date)

  def Medical(self):
    if (not self._pilot.birthday or
        not self._pilot.last_medical or
        self._pilot.medical_type not in (2, 3)):
      return

    if self._pilot.birthday < (self._today - self._40years):
      medical_due = self._pilot.last_medical + self._over40_medical_time
    else:
      medical_due = self._pilot.last_medical + self._under40_medical_time
    return self._Status(self._LastDayOfMonth(medical_due), 90)

  def FlightReview(self):
    if not self._bfr:
      return
    bfr_due = self._LastDayOfMonth(
        self._bfr.date + datetime.timedelta(days=365.2425 * 2))
    return self._Status(bfr_due, 60)

  def Tailwheel(self):
    if not self._pilot.last_tailwheel_flight:
      return
    due = self._pilot.last_tailwheel_flight.date() + datetime.timedelta(days=60)
    return self._Status(due, 30)

  def Cessna(self):
    if not self._pilot.last_cessna_flight:
      return
    due = self._pilot.last_cessna_flight.date() + datetime.timedelta(days=90)
    return self._Status(due, 30)

  def Night(self):
    if not self._pilot.last_night_flight:
      return
    due = self._pilot.last_night_flight.date() + datetime.timedelta(days=90)
    return self._Status(due, 30)

  def Instrument(self):
    if not self._pilot.instrument or not self._stats.recent_approaches:
      return
    due = self._stats.recent_approaches[0].date() + datetime.timedelta(days=182)
    return (self._Status(due, 30), self._stats.recent_approaches)


class InvalidInput(Exception):
  pass


class BadRequest(Exception):
  pass


def JsonifyDict(d):
  for k, v in d.iteritems():
    if isinstance(v, ndb.Key):
      model_dict = v.get().to_dict()
      d[k] = model_dict
      JsonifyDict(model_dict)
    elif isinstance(v, list):
      for i in v:
        if isinstance(i, ndb.Key):
          model_dict = i.get().to_dict()
          new_list.append(model_dict)
          JsonifyDict(model_dict)
    elif isinstance(v, datetime.date):
      d[k] = v.isoformat()
  return d


def Haversine(src, dst):
  earth_radius = 6371  # km

  # Convert to radians
  src_lat_rads = math.radians(src.lat)
  dst_lat_rads = math.radians(dst.lat)
  delta_lat = math.radians(dst.lat - src.lat)
  delta_lon = math.radians(dst.lon - src.lon)

  # a = sin²(Δφ/2) + cos(φ1).cos(φ2).sin²(Δλ/2)
  a = math.sin(delta_lat / 2)**2 + (
      math.cos(src_lat_rads) * math.cos(dst_lat_rads) \
      * math.sin(delta_lon / 2)**2)

  # c = 2.atan2(√a, √(1−a))
  c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
  return earth_radius * c


def UpdateXcHours(flight):
  if not flight.start_airport or not flight.end_airport:
    return
  start_airport = flight.start_airport.get()
  airport_keys = []
  airports = []
  airport_keys.append(flight.end_airport)
  for a in flight.intermediate_airports:
    airport_keys.append(a)
  is_xc = False
  for airport in ndb.get_multi(airport_keys):
    if start_airport.latlong and airport.latlong:
      d = Haversine(start_airport.latlong, airport.latlong)
      logging.info('Distance between %s and %s is %s' % (
          start_airport.key.id(), airport.key.id(), d))
      if d >= XC_REQUIRED_DISTANCE:
        is_xc = True
        break
  if is_xc:
    flight.xc_hours = flight.pic_hours


class BaseHandler(webapp2.RequestHandler):

  _template = None  # Must specify in subclass.

  def __init__(self, request, response):
    self.initialize(request, response)
    self.data = {}
    self.data['aircraft_categories'] = models.AIRCRAFT_CATEGORIES
    self.data['aircraft_classes'] = models.AIRCRAFT_CLASSES
    self.data['engine_types'] = models.ENGINE_TYPES
    self.data['search_uri'] = None
    self._GetPilot()
    self.page = int(self.request.get('page', 1))

  def NextCursor(self):
    cursor_key = '%s-%s-%s' % (
        self.pilot.user.email(), self._template, self.page)
    return memcache.get(cursor_key)

  def SetCursor(self, next_cursor):
    key = '%s-%s-%s' % (self.pilot.user.email(), self._template, self.page + 1)
    memcache.set(key, next_cursor)
    self.data['next_page'] = self.page + 1
    if self.page > 1:
      self.data['prev_page'] = self.page - 1

  def _GetPilot(self):
    user = users.get_current_user()
    if user:
      self.data['login_url'] = users.create_logout_url('/')
      self.data['login_text'] = 'Logout'
      pilot_key = ndb.Key('Pilot', user.user_id())
      pilot = pilot_key.get()
      if not pilot:
        logging.info('Creating a new pilot model for %s', user.email())
        pilot = models.Pilot(key=pilot_key)
        pilot.user = user
        pilot.put()
      self.pilot = pilot
      self.data['pilot'] = pilot
    else:
      self.data['login_url'] = users.create_login_url(self.request.uri)
      self.data['login_text'] = 'Login'

  def _DeleteModel(self, urlsafe_key):
    key = ndb.Key(urlsafe=urlsafe_key)
    if key:
      logging.info('Deleting model with key: %s', urlsafe_key)
      key.delete()

  def _UpdateAirportInfo(self, airport):
    taskqueue.add(
        url='/airport',
        params={'pilot': self.pilot.key.urlsafe(),
                'airport': airport.key.id()})

  def _UpdatePilotCurrency(self):
    key = 'stats_delay_%s' % self.pilot.key.urlsafe()
    delay = datetime.datetime.now() + datetime.timedelta(minutes=5)
    if not memcache.get(key):
      memcache.set(key, delay)
      taskqueue.add(
          eta=delay,
          url='/updatecurrency',
        params={'pilot': self.pilot.key.urlsafe(), 'delay_key': key})

  def Respond(self):
    tmpl = jinja_environment.get_template(self._template)
    self.response.out.write(tmpl.render(self.data))


class FlightsHandler(BaseHandler):

  _template = 'flights.html'

  def __init__(self, request, response):
    BaseHandler.__init__(self, request, response)

  def get(self):
    if 'pilot' not in self.data:
      self.Respond()
      return

    pilot_key = self.data['pilot'].key

    aircraft_query = models.Aircraft.query(ancestor=pilot_key)
    aircraft = aircraft_query.fetch(keys_only=True)
    self.data['aircraft'] = [a.id() for a in aircraft]

    query = models.Flight.query(ancestor=pilot_key).order(-models.Flight.date)
    start_cursor = self.NextCursor()
    flights, next_cursor, more = query.fetch_page(20, start_cursor=start_cursor)
    self.data.setdefault('flights', []).extend(flights)

    if next_cursor and more:
      self.SetCursor(next_cursor)
    self.Respond()

  def post(self):
    logging.info(self.request)
    if self.request.get('add-flight'):
      try:
        self._AddFlight()
      except InvalidInput as e:
        self.data.setdefault('errors', []).append(e)
        self.get()
        return
    flight_to_delete = self.request.get('delete')
    if flight_to_delete:
      self._DeleteModel(flight_to_delete)
      return
    self.get()

  def _AddFlight(self):
    pilot_key = self.data['pilot'].key
    flight = models.Flight(parent=pilot_key)
    flight.pilot = pilot_key

    date = self.request.get('date')
    if date:
      try:
        flight.date = datetime.datetime.strptime(date, DATE_FMT)
      except ValueError:
        raise InvalidInput('Error: date does not match %s' % DATE_FMT)

    registration = self.request.get('aircraft').upper().strip()
    if registration:
      aircraft = models.Aircraft.get_or_insert(
          registration, parent=pilot_key, registration=registration)
      flight.aircraft = aircraft.key
    else:
      return

    start_airport = self.request.get('start_airport')
    if start_airport:
      airport = models.Airport.get_or_insert(
          start_airport.upper(), parent=pilot_key)
      if not airport.name:
        self._UpdateAirportInfo(airport)
      flight.start_airport = airport.key
    end_airport = self.request.get('end_airport')
    if end_airport:
      airport = models.Airport.get_or_insert(
          end_airport.upper(), parent=pilot_key)
      flight.end_airport = airport.key
      if not airport.name:
        self._UpdateAirportInfo(airport)

    flight.pic_hours = float(self.request.get('hours', 0))
    flight.put()
    self.data.setdefault('flights', []).append(flight)


class SingleFlightHandler(BaseHandler):

  _template = 'flight.html'

  def __init__(self, request, response):
    BaseHandler.__init__(self, request, response)

  def get(self):
    flight = ndb.Key(urlsafe=self.request.get('f')).get()
    if not flight:
      logging.error('Flight not found: %s', self.request.get('f'))
      return
    self.data['flight'] = flight
    upload_url_rpc = blobstore.create_upload_url_async('/upload')

    self.data['airports'] = []
    self.data['telemetry'] = []
    self.data['landings'] = {}
    flight_dict = flight.to_dict()
    keys = []
    for value in flight.to_dict().values():
      if isinstance(value, ndb.Key):
        keys.append(value)
      if isinstance(value, list):
        keys.extend([x for x in value if isinstance(x, ndb.Key)])
    for model in ndb.get_multi(keys):
      if not model:
        continue
      if isinstance(model, models.Airport):
        self.data['airports'].append(model)
      if model.key == flight.start_airport and 'start_airport' not in self.data:
        self.data['start_airport'] = model
      elif model.key == flight.end_airport and 'end_airport' not in self.data:
        self.data['end_airport'] = model
      elif model.key == flight.aircraft:
        self.data['aircraft'] = model
      elif model.key == flight.instructor:
        self.data['instructor'] = model
      elif model.key in flight.telemetry:
        self.data['telemetry'].append(model)
      elif isinstance(model, models.Landing):
        airport = model.airport.id()
        runway = model.runway
        self.data['landings'].setdefault(airport, {}).setdefault(runway, 0)
        self.data['landings'][airport][runway] += 1

    self.data['upload_url'] = upload_url_rpc.get_result()
    self.Respond()

  def post(self):
    flight_key = self.request.get('flight')
    if not flight_key:
      logging.error('Flight key is required')
      return
    flight = ndb.Key(urlsafe=flight_key).get()
    for key, value in self.request.POST.items():
      if key.endswith('hours') and key in flight.to_dict().keys():
        if value:
          setattr(flight, key, float(value))
        else:
          delattr(flight, key)

    if not flight.xc_hours:
      UpdateXcHours(flight)

    filed_route = self.request.get('filed_route')
    if filed_route:
      flight.filed_route = filed_route
    else:
      del flight.filed_route
    remarks = self.request.get('remarks')
    if remarks:
      flight.remarks = remarks
    else:
      del flight.remarks
    cost = self.request.get('cost')
    if cost:
      flight.cost = float(cost)
    else:
      del flight.cost

    del flight.passengers
    for passenger in self.request.get_all('passenger'):
      if passenger:
        flight.passengers.append(passenger)

    flight.put()
    self._UpdatePilotCurrency()


class LandingHandler(BaseHandler):

  def __init__(self, request, response):
    BaseHandler.__init__(self, request, response)

  @staticmethod
  def MatchedLandings(landings, airport, runway, night):
    matched = []
    for landing in landings:
      logging.info('Landing: %s %s %s' % (landing.airport.id(),
                                          landing.runway, landing.night))
      if (landing.airport == airport.key and
          landing.runway == runway and
          landing.night == night):
        matched.append(landing)
    logging.info('Found %s matched runways.' % len(matched))
    return matched

  def _DeleteOneNoneRunway(self, flight, airport):
    for l in flight.landings:
      landing = l.get()
      if landing.airport == airport.key and landing.runway == None:
        logging.info('Deleting %s' % l)
        flight.landings.remove(l)
        l.delete()
        return

  def _UpdateLandings(self, flight, airport, runway, night, count):
    logging.info(
      'Update Landings: airport=%s runway=%s night=%s count=%s' % (
            airport.key, runway, night, count))
    landings = models.Landing.LandingsByFlight(flight).fetch()
    for landing in self.MatchedLandings(landings, airport, runway, night):
      count -= 1

    if count < 0:  # Number of landings was decreased.
      for landing in self.MatchedLandings(landings, airport, runway, night):
        if count < 0:
          flight.landings.remove(landing.key)
          logging.info('count=%s deleted=%s' % (count, landing.key))
          landing.key.delete()
        count += 1
    else:  # Number of landings was increased.
      for i in range(count):
        landing = models.Landing(parent=flight.key)
        landing.airport = airport.key
        landing.runway = runway
        landing.night = night
        landing.put()
        logging.info('Added %s' % landing)
        flight.landings.append(landing.key)
        self._DeleteOneNoneRunway(flight, airport)

  def _UpdateAirports(self, flight, airport):
    if (airport.key != flight.start_airport and
        airport.key != flight.end_airport and
        airport.key not in flight.intermediate_airports):
      flight.intermediate_airports.append(airport.key)
    if not airport.name:
      self._UpdateAirportInfo(airport)

  def _DeleteLandings(self, flight, airport, runway, night):
    for landing_key in flight.landings[:]:
      landing = landing_key.get()
      if (landing.airport == airport.key and
          landing.runway == runway and
          landing.night == night):
        flight.landings.remove(landing_key)
        logging.info('Deleting %s' % landing_key)
        landing_key.delete()

  def post(self):
    flight_key = self.request.get('flight')
    flight = ndb.Key(urlsafe=flight_key).get()
    runway = self.request.get('runway').upper()
    landings = int(self.request.get('landings'))
    night = bool(self.request.get('night'))
    airport_id = self.request.get('airport').upper()
    airport = models.Airport.get_or_insert(airport_id, parent=self.pilot.key)
    if not flight or not airport or not runway or landings is None:
      logging.error(
          ('Missing required fields. '
           'flight=%s airport=%s runway=%s landings=%s'
           % (flight, airport, runway, landings)))
      return

    if landings == 0:
      self._DeleteLandings(flight, airport, runway, night)
      data = {'deleted': True}
    else:
      self._UpdateAirports(flight, airport)
      # This landing may qualify the flight as XC.
      if not flight.xc_hours:
        UpdateXcHours(flight)
      self._UpdateLandings(flight, airport, runway, night, landings)
      data = {
          'airport': airport_id,
          'landings': landings,
          'runway': runway,
      }

    flight.put()
    self.response.out.write(json.dumps(data))


class ApproachHandler(BaseHandler):

  def __init__(self, request, response):
    BaseHandler.__init__(self, request, response)

  def post(self):
    flight_key = self.request.get('flight')
    flight = ndb.Key(urlsafe=flight_key).get()
    approaches = []
    for approach in self.request.get_all('approach'):
      if approach:
        approaches.append(approach.upper())
    flight.approaches = approaches
    flight.put()


def UpdateInstructor(pilot_key, request):
    name = request.get('name')
    if not name:
      raise BadRequest('Instructor name is required.')
    instructor_key = request.get('instructor')
    if instructor_key:
      instructor = ndb.Key(urlsafe=instructor_key).get()
    else:
      instructor = models.Instructor.get_or_insert(name, parent=pilot_key)

    instructor.name = name
    cert = request.get('cert')
    if cert:
      instructor.cert = cert
    signature = request.get('signature')
    if signature:
      instructor.signature = signature
    exp = request.get('expiration')
    if exp:
      try:
        expiration = datetime.datetime.strptime(exp, DATE_FMT)
      except ValueError:
        raise BadRequest('Date "%s" date does not match %s' % (exp, DATE_FMT))
      instructor.expiration = expiration

    instructor.put()
    return instructor


class InstructorHandler(BaseHandler):

  def __init__(self, request, response):
    BaseHandler.__init__(self, request, response)

  def post(self):
    try:
      instructor = UpdateInstructor(self.pilot.key, self.request)
    except BadRequest, e:
      logging.error(e)
      self.response.status = '400 %s' % e
      return

    flight_key = self.request.get('flight')
    if flight_key:
      logging.info(flight_key)
      flight = ndb.Key(urlsafe=flight_key).get()
      if flight.instructor != instructor.key:
        flight.instructor = instructor.key
        flight.put()
    instructor_dict = JsonifyDict(instructor.to_dict())
    instructor_dict['instructor'] = instructor.key.urlsafe()
    self.response.out.write(json.dumps(instructor_dict))


class AirportHandler(BaseHandler):

  _template = 'airports.html'

  def __init__(self, request, response):
    BaseHandler.__init__(self, request, response)
    self.runway_numbers = []
    self.data['search_uri'] = 'airports'

  def GetAirportById(self, airport_id):
    airport_id = airport_id.upper().strip()
    airport = models.Airport.get_by_id(airport_id, parent=self.pilot.key)
    if airport:
      self.data['airports'] = [airport]

  @staticmethod
  def OppositeRunway(runway):
    number_str, side = RUNWAY_RE.match(runway.number).groups()
    number = int(number_str)
    if number <= 18:
      number += 18
    else:
      number -= 18
    number = '{0:02d}'.format(number)
    if side:
      if side == 'L':
        number += 'R'
      elif side == 'R':
        number += 'L'
      elif side == 'C':
        number += 'C'
    new_runway = models.Runway(**runway.to_dict())
    new_runway.number = number
    return new_runway

  @staticmethod
  def BuildRunway(encoded_runway):
    number, length, width, surface = encoded_runway.split(':')
    if not number:
      return
    if not RUNWAY_RE.match(number):
      raise InvalidInput('Invalid runway: %s' % number)
    number = number.upper()
    runway = models.Runway()
    runway.number = number
    runway.length = int(length)
    runway.width = int(width)
    runway.surface = surface
    return runway

  def GetAllAirports(self):
    start = self.request.get('start', None)
    query = models.Airport.query(ancestor=self.pilot.key)
    query.order(-models.Airport.key)
    cursor = self.NextCursor()
    airports, next_cursor, more = query.fetch_page(20, start_cursor=cursor)
    if next_cursor and more:
      self.SetCursor(next_cursor)
    self.data['airports'] = airports

  def get(self):
    airport = self.request.get('airport')
    airport_id = self.request.get('q')
    if airport:
      self.data['airport'] = ndb.Key(urlsafe=airport).get()
    if airport_id:
      self.GetAirportById(airport_id)
    else:
      self.GetAllAirports()
    self.Respond()

  def post(self):
    logging.info(self.request)
    pilot = self.data['pilot']
    airport_key = self.request.get('airport')
    if not pilot or not airport_key:
      return

    airport = ndb.Key(urlsafe=airport_key).get()
    name = self.request.get('name')
    if name:
      airport.name = self.request.get('name')
    location = self.request.get('location')
    if location:
      airport.location = location
    latlong = self.request.get('latlong')
    if latlong:
      airport.latlong = ndb.GeoPt(latlong)
    runways = []
    for encoded_runway in self.request.get_all('runway'):
      runway = self.BuildRunway(encoded_runway)
      if not runway:
        continue
      if runway.number not in self.runway_numbers:
        self.runway_numbers.append(runway.number)
        runways.append(runway)
      opposite_runway = self.OppositeRunway(runway)
      if opposite_runway.number not in self.runway_numbers:
        self.runway_numbers.append(opposite_runway.number)
        runways.append(opposite_runway)
    airport.runways = runways
    airport.put()


class FlightPhotoUploadHandler(blobstore_handlers.BlobstoreUploadHandler):

  def post(self):
    flight = ndb.Key(urlsafe=self.request.get('flight')).get()
    if not flight:
      blobstore.delete([b.key() for b in self.get_uploads('file')])
    else:
      for photo in self.get_uploads('upload'):
        flight.photos.append(photo.key())
        img_url = images.get_serving_url(photo.key(), size=1000)
        flight.photo_urls.append(img_url)
      flight.put()
    self.redirect('/flight?f=%s' % flight.key.urlsafe())


class PilotPhotoUploadHandler(blobstore_handlers.BlobstoreUploadHandler):

  def post(self):
    pilot = ndb.Key(urlsafe=self.request.get('pilot')).get()
    blob_info = self.get_uploads('upload')[0]
    if blob_info:
      img_url = images.get_serving_url(blob_info.key(), size=500)
      if pilot.photo:
        blobstore.delete(pilot.photo)
      pilot.photo = blob_info.key()
      pilot.photo_url = img_url
      pilot.put()
    self.redirect('/pilot')


class TripsHandler(webapp2.RequestHandler):

  def get(self):
    pass

  def post(self):
    pass


class AircraftHandler(BaseHandler):

  _template = 'aircraft.html'

  def __init__(self, request, response):
    BaseHandler.__init__(self, request, response)
    self.data['search_uri'] = 'aircraft'

  def GetAircraft(self, registration):
    registration = registration.upper().strip()
    aircraft = models.Aircraft.get_by_id(registration, parent=self.pilot.key)
    if aircraft:
      return [aircraft]

  def get(self):
    registration = self.request.get('q')
    if registration:
      aircraft = self.GetAircraft(registration)
    else:
      cursor = self.NextCursor()
      query = models.Aircraft.query(ancestor=self.pilot.key)
      query.order(-models.Aircraft.key)
      aircraft, next_cursor, more = query.fetch_page(20, start_cursor=cursor)
      if next_cursor and more:
        self.SetCursor(next_cursor)
    self.data['aircraft'] = aircraft

    aircraft_urlsafe = self.request.get('aircraft')
    if aircraft_urlsafe:
      aircraft_key = ndb.Key(urlsafe=aircraft_urlsafe)
      if self.request.get('delete'):
        logging.info('Deleting aircraft: %s' % aircraft_urlsafe)
        aircraft_key.delete()
      else:
        self.data['edit_aircraft'] = aircraft_key.get()

    self.Respond()

  def post(self):
    logging.info(self.request)
    pilot_key = self.pilot.key
    aircraft_to_delete = self.request.get('delete')
    if aircraft_to_delete:
      self._DeleteModel(aircraft_to_delete)
      return

    aircraft_urlsafe = self.request.get('aircraft')
    if aircraft_urlsafe:
      aircraft = ndb.Key(urlsafe=aircraft_urlsafe).get()
    else:
      registration = self.request.get('registration').upper().strip()
      if not registration:
        logging.error('Registration or aircraft key is required.')
        return
      aircraft = models.Aircraft.get_or_insert(registration, parent=pilot_key)
      aircraft.registration = registration

    aircraft.aircraft_class = self.request.get('aircraft_class')
    aircraft.category = self.request.get('category')
    aircraft.engine_type = self.request.get('engine_type')
    aircraft.make = self.request.get('make')
    aircraft.model = self.request.get('model')
    aircraft.num_engines = int(self.request.get('num_engines'))
    aircraft.tailwheel = bool(self.request.get('tailwheel'))
    aircraft.retractable_gear = bool(self.request.get('retractable_gear'))
    aircraft.controllable_prop = bool(self.request.get('controllable_prop'))
    aircraft.high_performance = bool(self.request.get('high_performance'))
    aircraft.has_flaps = bool(self.request.get('has_flaps'))
    aircraft_key = aircraft.put()
    self.get()


class PilotHandler(BaseHandler):

  _template = 'pilot.html'

  def __init__(self, request, response):
    BaseHandler.__init__(self, request, response)

  def CalculateCurrency(self):
    pilot_currency = Currency(self.pilot, self.data['bfr'], self.data['stats'])
    self.data['medical_due'] = pilot_currency.Medical()
    self.data['bfr_due'] = pilot_currency.FlightReview()
    self.data['tailwheel_due'] = pilot_currency.Tailwheel()
    self.data['cessna_due'] = pilot_currency.Cessna()
    self.data['night_due'] = pilot_currency.Night()
    instrument_currency = pilot_currency.Instrument()
    if instrument_currency:
      self.data['ifr_due'] = instrument_currency[0]
      self.data['recent_approaches'] = instrument_currency[1]

  def get(self):
    upload_url_rpc = blobstore.create_upload_url_async('/pilotphoto')
    self.data['upload_url'] = upload_url_rpc.get_result()
    self.data['endorsements'] = [x.get() for x in self.pilot.endorsements]
    if self.pilot.stats:
      self.data['stats'] = self.pilot.stats.get()
    if self.pilot.bfr:
      self.data['bfr'] = self.pilot.bfr.get()
      self.data['bfr_instructor'] = self.data['bfr'].instructor.get()
      self.CalculateCurrency()
    self.Respond()

  def post(self):
    for key, value in self.request.POST.items():
      logging.info(type(value))
      logging.info('key=%s value=%s' % (key, value))
      if key in self.pilot._properties and value:
        prop = self.pilot._properties[key]
        if isinstance(prop, ndb.StringProperty):
          setattr(self.pilot, key, value)
        elif isinstance(prop, ndb.DateProperty):
          setattr(self.pilot, key, datetime.datetime.strptime(value, DATE_FMT))
        elif isinstance(prop, ndb.BooleanProperty):
          setattr(self.pilot, key, value == 'true')
        elif isinstance(prop, ndb.IntegerProperty):
          setattr(self.pilot, key, int(value))
    self.pilot.put()


class PilotCurrencyHandler(webapp2.RequestHandler):

  @ndb.toplevel
  def post(self):
    if not self.request.get('pilot'):
      self.response.status = '400 No pilot specified'
      return

    self.pilot = ndb.Key(urlsafe=self.request.get('pilot')).get()
    if self.pilot.stats:
      self.pilot.stats.delete_async()
      self.pilot.stats = None
    num_landings = models.Landing.query(ancestor=self.pilot.key).count_async()
    flights = models.Flight.query(
        ancestor=self.pilot.key).order(models.Flight.date).map(self.flight_aircraft)

    if not self.pilot.stats:
      self._NewPilotStats()
    else:
      self.stats = self.pilot.stats.get()

    self.stats.num_landings = num_landings.get_result()

    self.UpdateInstrumentCurrency(flights)
    for flight, aircraft in flights:
      if not aircraft:
        logging.error('Found flight with no aircraft: %s' % flight.key.urlsafe())
        continue
      self.stats.total_num_flights += 1
      self.UpdatePilotCurrency(flight, aircraft)
      self.UpdateHourStats(flight)
      self.UpdateAircraftStats(flight, aircraft)
      self.UpdateMiscStats(flight)
    self.stats.put_async()
    self.pilot.put_async()
    memcache.delete(self.request.get('delay_key'))

  @ndb.tasklet
  def flight_aircraft(self, flight):
    aircraft = yield flight.aircraft.get_async()
    raise ndb.Return((flight, aircraft))

  def _NewPilotStats(self):
    self.stats = models.FlightStats(parent=self.pilot.key)
    self.stats.put()
    self.pilot.stats = self.stats.key

  def UpdateInstrumentCurrency(self, flights):
    approaches = deque([], 6)
    for flight, _ in flights:
      for approach in flight.approaches:
        approaches.append(flight.date)
    self.stats.recent_approaches = list(approaches)
    logging.info('Recent approaches: %r' % approaches)

  def UpdatePilotCurrency(self, flight, aircraft):
    if aircraft.tailwheel:
      if (not self.pilot.last_tailwheel_flight or
          flight.date > self.pilot.last_tailwheel_flight):
        self.pilot.last_tailwheel_flight = flight.date
    elif aircraft.make and aircraft.make.lower() == 'cessna':
      if (not self.pilot.last_cessna_flight or
          flight.date > self.pilot.last_cessna_flight):
        self.pilot.last_cessna_flight = flight.date
    if flight.night_hours:
      if (not self.pilot.last_night_flight or
          flight.date > self.pilot.last_night_flight):
        self.pilot.last_night_flight = flight.date

  def UpdateAircraftStats(self, flight, aircraft):
    if not flight.duration:
      return
    make_model = aircraft.MakeAndModel()
    for aircraft_stat in self.stats.aircraft_stats:
      if aircraft_stat.make_model == make_model:
        aircraft_stat.hours += flight.duration
        break
    else:
      new_stat = models.AircraftStat(
          make_model=make_model, hours=flight.duration)
      self.stats.aircraft_stats.append(new_stat)

    if aircraft.tailwheel:
      self.stats.tailwheel_hours += flight.duration

  def UpdateMiscStats(self, flight):
    if flight.approaches:
      self.stats.num_approaches += len(flight.approaches)
    if flight.cost:
      self.stats.total_cost += flight.cost

  def _IncrementHours(self, flight, hour_type):
    hours = getattr(flight, hour_type)
    if hours:
      stats_hours = getattr(self.stats, hour_type)
      setattr(self.stats, hour_type, stats_hours + hours)

  def UpdateHourStats(self, flight):
    hour_types = (
        'dual_hours', 'ground_instruction_hours', 'imc_hours', 'night_hours',
        'pic_hours', 'sim_imc_hours', 'simulator_hours', 'xc_hours')
    for hour_type in hour_types:
      self._IncrementHours(flight, hour_type)
    if flight.duration:
      self.stats.total_hours += flight.duration


class EndorsementHandler(BaseHandler):

  _template = 'pilot.html'

  def __init__(self, request, response):
    BaseHandler.__init__(self, request, response)

  def post(self):
    endorsement_key = self.request.get('endorsement')
    if endorsement_key:
      endorsement = ndb.Key(urlsafe=endorsement_key).get()
    else:
      endorsement = models.Endorsement(parent=self.pilot.key)

    if self.request.get('delete'):
      self.pilot.endorsements.remove(endorsement.key)
      self.pilot.put()
      endorsement.key.delete()
      return

    date = self.request.get('date')
    if date:
      try:
        endorsement.date = datetime.datetime.strptime(date, DATE_FMT)
      except ValueError:
        raise InvalidInput('Error: date does not match %s' % DATE_FMT)
    details = self.request.get('details')
    if details:
      endorsement.details = details
    instructor_name = self.request.get('name')
    if instructor_name:
      instructor = models.Instructor.get_or_insert(
          instructor_name, name=instructor_name, parent=self.pilot.key)
      cert = self.request.get('cert')
      if cert:
        instructor.cert = cert
      exp = self.request.get('expiration')
      if exp:
        try:
          instructor.expiration = datetime.datetime.strptime(exp, DATE_FMT)
        except ValueError:
          raise InvalidInput('Error: date does not match %s' % DATE_FMT)
      signature = self.request.get('signature')
      if signature:
        instructor.signature = signature
      instructor.put()
      endorsement.instructor = instructor.key

    endorsement.put()
    if self.request.get('bfr'):
      self.pilot.bfr = endorsement.key
    else:
      if endorsement.key not in self.pilot.endorsements:
        self.pilot.endorsements.append(endorsement.key)
    self.pilot.put()
    self.response.out.write(json.dumps(JsonifyDict(endorsement.to_dict())))


class AirportFetchHandler(webapp2.RequestHandler):

  def __init__(self, request, response):
    webapp2.RequestHandler.__init__(self, request, response)
    self.url_spec = 'http://vfrmap.com/fe?req=get_afd&q=%s'

  def Name(self):
    return self.soup.findAll(
        'div', {'class': 'afd_div_0'})[0].find('span').string.title()

  def LatLong(self):
    ll_str = self.soup.findAll(
        'div', {'class': 'afd_div_0'})[0].findAll('span')[2].contents[-1]
    latitude, longitude = ll_str.split(' ')
    match_lat = GEO_RE.match(latitude)
    match_lon = GEO_RE.match(longitude)
    if not match_lat and not match_lon:
      logging.error(ll_str)
      return
    lat_c, lat_d, lat_m = match_lat.groups()
    latitude = (float(lat_m) / 60) + int(lat_d)
    latitude = latitude * CARDINAL_MULTIPLIER[lat_c]
    lon_c, lon_d, lon_m = match_lon.groups()
    longitude = (float(lon_m) / 60) + int(lon_d)
    longitude = longitude * CARDINAL_MULTIPLIER[lon_c]
    return latitude, longitude

  def Location(self):
    return self.soup.find('span', {'class': 'afd_font_city'}).string.title()

  def AddRunways(self, airport):
    runway_re = re.compile('RWY (\d\d?[RLC]?).(\d\d?[RLC]?).*', re.U)
    for r in self.soup.findAll(text=runway_re):
      numbers = runway_re.match(r).groups()
      data = r.parent.nextSibling.nextSibling.text
      paved, length, width = re.match('^(H?)(\d+)X(\d+).*', data).groups()
      for n in numbers:
        runway = models.Runway()
        runway.number = n
        runway.length = int(length)
        runway.width = int(width)
        if paved:
          runway.surface = 'paved'
        else:
          runway.surface = 'turf'
        airport.runways.append(runway)

  def post(self):
    pilot = ndb.Key(urlsafe=self.request.get('pilot'))
    airport_id = self.request.get('airport', '').upper()
    logging.info('Fetching airport info for %s' % airport_id)
    url = self.url_spec % airport_id
    airport = models.Airport.get_by_id(airport_id, parent=pilot)
    if not airport:
      logging.error('Airport not found: %s' % airport_id)
      self.response.status_int = 400
      return

    try:
      result = urllib2.urlopen(url)
    except urllib2.URLError, e:
      logging.error(e)
      self.response.status_int = 500
      return

    self.soup = BeautifulSoup(result)
    if not airport.name:
      airport.name = self.Name()
    if not airport.location:
      airport.location = self.Location()
    if True or not airport.latlong:
      latitude, longitude = self.LatLong()
      airport.latlong = ndb.GeoPt(latitude, longitude)
    if not airport.runways:
      self.AddRunways(airport)
    logging.info(JsonifyDict(airport.to_dict()))
    airport.put()


class XcHandler(BaseHandler):

  _template = 'xc.html'

  def __init__(self, request, response):
    BaseHandler.__init__(self, request, response)

  def get(self):
    query = models.Flight.query(ancestor=self.pilot.key).filter(models.Flight.xc_hours > 0.0)
    flights = query.fetch()
    flights = filter(lambda x: x.pic_hours > 0.0, flights)
    flights.sort(key=lambda f: f.date)
    self.data['flights'] = flights
    self.data['landings'] = {}
    for f in flights:
      for l in f.landings:
        l = l.get()
        self.data['landings'].setdefault(
            f.key.urlsafe(), set()).add(l.airport.id())
    total_xc = 0
    for f in flights:
      total_xc += f.xc_hours
    self.data['total_xc'] = total_xc
    self.Respond()


class IfrHandler(BaseHandler):

  _template = 'ifr.html'

  def __init__(self, request, response):
    BaseHandler.__init__(self, request, response)

  def get(self):
    query = models.Flight.query(ancestor=self.pilot.key).filter(models.Flight.date > datetime.datetime(2011, 11, 01))
    flights = query.fetch()
    flights = filter(lambda x: x.simulator_hours or x.imc_hours or x.sim_imc_hours, flights)
    self.data['flights'] = flights
    self.data['landings'] = {}
    for f in flights:
      for l in f.landings:
        l = l.get()
        self.data['landings'].setdefault(
            f.key.urlsafe(), set()).add(l.airport.id())
    total_sim = 0
    total_imc = 0
    total_sim_imc = 0
    for f in flights:
      if f.simulator_hours:
        total_sim += f.simulator_hours
      if f.imc_hours:
        total_imc += f.imc_hours
      if f.sim_imc_hours:
        total_sim_imc += f.sim_imc_hours
    self.data['total_imc'] = total_imc
    self.data['total_sim_imc'] = total_sim_imc
    self.data['total_sim'] = total_sim
    self.Respond()


LogRow = namedtuple('LogRow', [
    'date', 'model', 'tail', 'start_airport', 'intermediate_airports',
    'filed_route', 'approaches', 'end_airport', 'remarks', 'day_landings',
    'night_landings', 'sel_hours', 'sim_hours', 'ground_hours', 'night_hours',
    'imc_hours', 'sim_imc_hours', 'xc_hours', 'dual_hours', 'pic_hours',
    'instructor', 'cost'])


class BootstrapHandler(BaseHandler):

  _template = 'flights.html'

  def __init__(self, request, response):
    BaseHandler.__init__(self, request, response)

  def get(self):
    logging.info(self.request)
    user = users.get_current_user()
    pilot_key = ndb.Key('Pilot', user.user_id())
    logging.info('Bootstrapping pilot: %s' % pilot_key.urlsafe())
    taskqueue.add(url='/bootstrap', params={'pilot': pilot_key.urlsafe()})
    self.redirect('/')

  def PopulateAirports(self):
    for airport in models.Airport.query(ancestor=self.pilot_key).fetch():
      taskqueue.add(
          url='/airport',
          params={'pilot': self.pilot_key.urlsafe(),
                  'airport': airport.key.id()})

  def PopulateFlights(self):
    with file('logbook.csv', 'rb') as logbook_file:
      parent = ndb.Key(urlsafe=self.request.get('pilot'))
      logbook = csv.reader(logbook_file)
      for row in logbook:
        log = LogRow(*row)
        flight = models.Flight(parent=parent)
        flight.put()
        flight.date = datetime.datetime.strptime(log.date, '%m/%d/%Y')
        aircraft = models.Aircraft.get_or_insert(
            log.tail.upper().strip(), parent=parent)
        if not aircraft.model:
          aircraft.pilot = parent
          aircraft.registration = log.tail.upper().strip()
          aircraft.model = log.model
          aircraft.num_engines = 1
          aircraft.aircraft_class = models.CLASS_L
          aircraft.category = models.AIRPLANE
          aircraft.engine_type = models.ENGINE_P
          if log.model in ['7ECA', '7GCAA']:
            aircraft.tailwheel = True
          elif log.model in ['C172', 'C152']:
            aircraft.make = 'Cessna'
          elif log.model in ['C162', 'C172SP-G1000']:
            aircraft.make = 'Cessna'
            aircraft.glass_panel = True
        aircraft.put()
        flight.aircraft = aircraft.key

        if log.start_airport:
          start_airport = models.Airport.get_or_insert(
              log.start_airport.upper().strip(), parent=parent)
          start_airport.put()
          flight.start_airport = start_airport.key
        if log.end_airport:
          end_airport = models.Airport.get_or_insert(
              log.end_airport.upper().strip(), parent=parent)
          end_airport.put()
          flight.end_airport = end_airport.key
        if log.intermediate_airports:
          for a in log.intermediate_airports.split(','):
            airport = models.Airport.get_or_insert(a.strip(), parent=parent)
            airport.put()
            flight.intermediate_airports.append(airport.key)

        if log.filed_route:
          flight.filed_route = log.filed_route.strip()
        if log.approaches:
          for a in log.approaches.split(','):
            flight.approaches.append(a)
        if log.remarks:
          flight.remarks = log.remarks.strip()
        if log.instructor:
          instructor = models.Instructor.get_or_insert(
              log.instructor.strip(), parent=parent)
          instructor.name = log.instructor.strip()
          instructor.put()
          flight.instructor = instructor.key
        if log.cost:
          flight.cost = float(log.cost)

        all_landings = []
        if log.day_landings:
          for i in range(int(log.day_landings)):
            landing = models.Landing(parent=flight.key)
            landing.airport = end_airport.key
            landing.night = False
            all_landings.append(landing)
        if log.night_landings:
          for i in range(int(log.night_landings)):
            landing = models.Landing(parent=flight.key)
            landing.airport = end_airport.key
            landing.night = True
            all_landings.append(landing)
        ndb.put_multi(all_landings)
        flight.landings = [x.key for x in all_landings]

        if log.sim_hours:
          flight.simulator_hours = float(log.sim_hours)
        if log.ground_hours:
          flight.ground_hours = float(log.ground_hours)
        if log.night_hours:
          flight.night_hours = float(log.night_hours)
        if log.imc_hours:
          flight.imc_hours = float(log.imc_hours)
        if log.sim_imc_hours:
          flight.sim_imc_hours = float(log.sim_imc_hours)
        if log.xc_hours:
          flight.xc_hours = float(log.xc_hours)
        if log.dual_hours:
          flight.dual_hours = float(log.dual_hours)
        if log.pic_hours:
          flight.pic_hours = float(log.pic_hours)

        # Save everything
        flight.put()

  @ndb.toplevel
  def post(self):
    logging.info(self.request)
    foo = models.Once.get_by_id('runonce')
    if foo:
      logging.warning('This can only be run once. Ignoring request.')
      return
    else:
      models.Once(id='runonce').put()
    self.pilot_key = ndb.Key(urlsafe=self.request.get('pilot'))
    #self.PopulateAirports()
    self.PopulateFlights()
    taskqueue.add(
        url='/updatecurrency', params={'pilot': self.request.get('pilot')})


app = webapp2.WSGIApplication(
    [
      ('/', FlightsHandler),
      ('/aircraft', AircraftHandler),
      ('/airport', AirportFetchHandler),
      ('/airports', AirportHandler),
      ('/bootstrap', BootstrapHandler),
      ('/endorsement', EndorsementHandler),
      ('/flight', SingleFlightHandler),
      ('/flights', FlightsHandler),
      ('/instructor', InstructorHandler),
      ('/landings', LandingHandler),
      ('/pilot', PilotHandler),
      ('/pilotphoto', PilotPhotoUploadHandler),
      ('/saveapproaches', ApproachHandler),
      ('/trips', TripsHandler),
      ('/updatecurrency', PilotCurrencyHandler),
      ('/upload', FlightPhotoUploadHandler),
      ('/xc', XcHandler),
      ('/ifr', IfrHandler),
    ], debug=True)
